policy.output_field_unproved
A query or action lets a caller see a field's value, directly or through what it decides, though not every caller may read that field.
Area 9, Policy; not declinable; the next step is usually the agent's.
Why Jadpo refuses it
A field's own policy: can narrow who may read it, below who may read
its record. An output type has one shape for everyone, so everyone who may
run a query or action gets every field it sends. If some of them may not
read one of those fields, the output would show it to them.
Who may read a value is proved from the grants alone, not from the data.
And a field's value reaches a caller in more ways than an output: through
a copy, a condition, a value or failure it decides, a record it finds, or
what a write stores for others to read later. Jadpo checks each of these
like an output, so a field's policy: can't be got round.
What it looks like
main.jadpo:
type Handle = Text { max_length: 40}entity User { id: Uuid handle: Handle { unique: true }}principal { user: { subject: User.handle }}type NoteText = Text { max_length: 200}entity Note { id: Uuid owner_id: User.id { on_delete: OnDelete.cascade } reviewer_id: User.id { on_delete: OnDelete.cascade } text: NoteText appraisal: NoteText { policy: { read: [reviewer_id] } } policy: { read: [owner_id, reviewer_id] }}type NoteView = Output { pick: [Note.id, Note.text, Note.appraisal]}query find_note(note_id: Note.id) fails NoteNotFound -> NoteView { from: Note where: Note.id == note_id missing: NoteNotFound}
error find_note sends Note.appraisal, which not every caller may read
--> main.jadpo:46:57
|46 | query find_note(note_id: Note.id) fails NoteNotFound -> NoteView { | ^^^^^^^^ carries `appraisal`
- reason
- Anyone
Note'sread:lets in may callfind_note, the userowner_idpoints at among them, and only the userreviewer_idpoints at may readappraisal. An output has one shape for everyone, so a field some callers may not read can't be in it. - next
- Return an output without
appraisalfromfind_note(agent) - rule
- policy.
output_ field_ unproved - help
- https://jadpo.
dev/docs/errors/policy. output_ field_ unproved
(agent) at the end of the next: line means your coding agent can make
the fix itself.
A note's owner may call find_note, since Note's read: names
owner_id, but only its reviewer may read the appraisal.
A value copied through a variable or another type keeps its field's rules. So the same appraisal, built into an output by an action, is refused there:
type Handle = Text { max_length: 40}entity User { id: Uuid handle: Handle { unique: true }}principal { user: { subject: User.handle }}type NoteText = Text { max_length: 200}entity Note { id: Uuid owner_id: User.id { on_delete: OnDelete.cascade } reviewer_id: User.id { on_delete: OnDelete.cascade } text: NoteText appraisal: NoteText { policy: { read: [reviewer_id] } } policy: { read: [owner_id, reviewer_id] }}type Assessment = Object { appraisal: Note.appraisal}type NoteView = Output { pick: [Note.id, Note.appraisal]}action show_note(note_id: Note.id) fails NoteNotFound -> NoteView { var note: Note = attempt Note.get(note_id) var assessment: Assessment = Assessment { appraisal: note.appraisal } return NoteView { id: note.id appraisal: assessment.appraisal }}
error NoteView.appraisal carries Note.appraisal, which not every caller of show_note may read
--> main.jadpo:57:20
|57 | appraisal: assessment.appraisal | ^^^^^^^^^^^^^^^^^^^^ from `Note.appraisal`
- reason
show_notereaches theNoteit comes from for anyone its grants let in, the userowner_idpoints at among them, and only the userreviewer_idpoints at may readappraisal. What a caller is sent has one shape for everyone, and a value passed through a variable or another type keeps its field's rules.- next
- Leave
appraisalout ofNoteView(agent) - rule
- policy.
output_ field_ unproved - help
- https://jadpo.
dev/docs/errors/policy. output_ field_ unproved
A condition shows the field it tests. So a list of the notes whose appraisal is "Excellent" is refused to their owners, who may not read it:
type Handle = Text { max_length: 40}entity User { id: Uuid handle: Handle { unique: true }}principal { user: { subject: User.handle }}type NoteText = Text { max_length: 200}entity Note { id: Uuid owner_id: User.id { on_delete: OnDelete.cascade } reviewer_id: User.id { on_delete: OnDelete.cascade } text: NoteText appraisal: NoteText { policy: { read: [reviewer_id] } } policy: { read: [owner_id, reviewer_id] }}type NoteView = Output { pick: [Note.id, Note.text]}query praised_notes() -> List<NoteView> { from: Note where: Note.appraisal == "Excellent" order: [Note.text asc] limit: 50}
error praised_notes filters on Note.appraisal, which not every caller may read
--> main.jadpo:48:12
|48 | where: Note.appraisal == "Excellent" | ^^^^^^^^^^^^^^ tests `appraisal`
- reason
- Anyone
Note'sread:lets in may see the recordspraised_notesreads, the userowner_idpoints at among them, and only the userreviewer_idpoints at may readappraisal. Which records pass a condition shows each one'sappraisal, so a query filters and orders only on fields every caller may read. - next
- Remove the condition on
Note.appraisal, or test a field every caller may read (agent) - rule
- policy.
output_ field_ unproved - help
- https://jadpo.
dev/docs/errors/policy. output_ field_ unproved
A value made under a condition shows what the condition tests. So a
verdict built from constants is refused when the if deciding it tests
the appraisal:
type Handle = Text { max_length: 40}entity User { id: Uuid handle: Handle { unique: true }}principal { user: { subject: User.handle }}type NoteText = Text { max_length: 200}entity Note { id: Uuid owner_id: User.id { on_delete: OnDelete.cascade } reviewer_id: User.id { on_delete: OnDelete.cascade } text: NoteText appraisal: NoteText { policy: { read: [reviewer_id] } } policy: { read: [owner_id, reviewer_id] }}type Laurel = Output { praised: Bool}action judge_note(note_id: Note.id) fails NoteNotFound -> Laurel { var note: Note = attempt Note.get(note_id) if (note.appraisal == "Excellent") { return Laurel { praised: true } } return Laurel { praised: false }}
error Laurel.praised carries Note.appraisal, which not every caller of judge_note may read
--> main.jadpo:50:22
|50 | praised: true | ^^^^ decided by `Note.appraisal`
- reason
judge_notereaches theNoteit comes from for anyone its grants let in, the userowner_idpoints at among them, and only the userreviewer_idpoints at may readappraisal. Which value is sent shows what decided it, so a value made under anif, amatchor a loop, or kept by a filter, keeps the rules of the fields they test.- next
- Decide
praisedby what every caller ofjudge_notemay read (agent) - rule
- policy.
output_ field_ unproved - help
- https://jadpo.
dev/docs/errors/policy. output_ field_ unproved
A failure raised on such a condition shows what it tests in the same way:
type Handle = Text { max_length: 40}entity User { id: Uuid handle: Handle { unique: true }}principal { user: { subject: User.handle }}type NoteText = Text { max_length: 200}entity Note { id: Uuid owner_id: User.id { on_delete: OnDelete.cascade } reviewer_id: User.id { on_delete: OnDelete.cascade } text: NoteText appraisal: NoteText { policy: { read: [reviewer_id] } } policy: { read: [owner_id, reviewer_id] }}type NoteView = Output { pick: [Note.id, Note.text]}type NotPraised = Failure { kind: FailureKind.conflict message: "Only a praised note can be pinned."}action pin_note(note_id: Note.id) fails NoteNotFound, NotPraised -> NoteView { var note: Note = attempt Note.get(note_id) if (note.appraisal != "Excellent") { reject NotPraised } return NoteView { id: note.id text: note.text }}
error pin_note raises NotPraised on a condition that tests Note.appraisal, which not every caller may read
--> main.jadpo:54:9
|54 | reject NotPraised | ^^^^^^^^^^^^^^^^^ decided by `Note.appraisal`
- reason
pin_notereaches theNoteit comes from for anyone its grants let in, the userowner_idpoints at among them, and only the userreviewer_idpoints at may readappraisal. WhetherNotPraisedis raised shows what the condition tested, so a failure is raised only on what every caller may read.- next
- Test a field every caller may read, or raise
NotPraisedonly where callers who may readappraisalreach (agent) - rule
- policy.
output_ field_ unproved - help
- https://jadpo.
dev/docs/errors/policy. output_ field_ unproved
Which record a get finds shows the value it is given. So a citation
only the reviewer may know can't be used to find its record for the
note's owner:
type Handle = Text { max_length: 40}entity User { id: Uuid handle: Handle { unique: true }}principal { user: { subject: User.handle }}type NoteText = Text { max_length: 200}entity Citation { id: Uuid title: NoteText policy: { read: [Access.authenticated] }}entity Note { id: Uuid owner_id: User.id { on_delete: OnDelete.cascade } reviewer_id: User.id { on_delete: OnDelete.cascade } citation_id: Citation.id { on_delete: OnDelete.restricted policy: { read: [reviewer_id] } } text: NoteText policy: { read: [owner_id, reviewer_id] }}type CitationView = Output { pick: [Citation.id, Citation.title]}action note_citation(note_id: Note.id) fails NoteNotFound, CitationNotFound -> CitationView { var note: Note = attempt Note.get(note_id) var citation: Citation = attempt Citation.get(note.citation_id) return CitationView { id: citation.id title: citation.title }}
error note_citation finds a Citation by Note.citation_id, which not every caller may read
--> main.jadpo:58:51
|58 | var citation: Citation = attempt Citation.get(note.citation_id) | ^^^^^^^^^^^^^^^^ from `Note.citation_id`
- reason
note_citationreaches theNoteit comes from for anyone its grants let in, the userowner_idpoints at among them, and only the userreviewer_idpoints at may readcitation_id. Whether aCitationis found by it, and what it holds, shows whatcitation_idholds, so a value used to find a record keeps its field's rules, as one sent in an output does.- next
- Find it by a value every caller of
note_citationmay read (agent) - rule
- policy.
output_ field_ unproved - help
- https://jadpo.
dev/docs/errors/policy. output_ field_ unproved
An include shows the reference it follows in the same way:
type Handle = Text { max_length: 40}entity User { id: Uuid handle: Handle { unique: true }}principal { user: { subject: User.handle }}type NoteText = Text { max_length: 200}entity Citation { id: Uuid title: NoteText policy: { read: [Access.authenticated] }}entity Note { id: Uuid owner_id: User.id { on_delete: OnDelete.cascade } reviewer_id: User.id { on_delete: OnDelete.cascade } citation_id: Citation.id { on_delete: OnDelete.restricted policy: { read: [reviewer_id] } } text: NoteText policy: { read: [owner_id, reviewer_id] }}type CitationView = Output { pick: [Citation.id, Citation.title]}type NoteWithCitation = Output { pick: [Note.id, Note.text] citation: CitationView}query note_with_citation(note_id: Note.id) fails NoteNotFound -> NoteWithCitation { from: Note where: Note.id == note_id missing: NoteNotFound include: { citation: Note.citation_id }}
error note_with_citation includes the Citation each Note refers to through citation_id, which not every caller may read
--> main.jadpo:66:9
|66 | citation: Note.citation_id | ^^^^^^^^ through `citation_id`
- reason
- Anyone
Note'sread:lets in may see the recordsnote_with_citationreads, the userowner_idpoints at among them, and only the userreviewer_idpoints at may readcitation_id. Which record an include reads shows the reference it follows, so a query includes only through references every caller may read. - next
- Remove the include, or give
Note.citation_id'sread:everyone who may read aNote(agent) - rule
- policy.
output_ field_ unproved - help
- https://jadpo.
dev/docs/errors/policy. output_ field_ unproved
A caller may change a record they may not read. So the record an update
gives back is proved against its entity's read::
type Handle = Text { max_length: 40}entity User { id: Uuid handle: Handle { unique: true }}principal { user: { subject: User.handle }}type NoteText = Text { max_length: 200}entity Note { id: Uuid owner_id: User.id { on_delete: OnDelete.cascade } editor_id: User.id { on_delete: OnDelete.cascade } text: NoteText summary: NoteText policy: { read: [owner_id] update: [owner_id, editor_id] }}type SummaryView = Output { pick: [Note.id, Note.summary]}action edit_note(note_id: Note.id, revised: NoteText) fails NoteNotFound -> SummaryView { var note: Note = attempt Note.update(note_id) { set: { text: revised } } return SummaryView { id: note.id summary: note.summary }}
error SummaryView.summary carries Note.summary, which not every caller of edit_note may read
--> main.jadpo:51:18
|51 | summary: note.summary | ^^^^^^^^^^^^ from `Note.summary`
- reason
edit_notereaches theNoteit comes from for anyone its grants let in, the usereditor_idpoints at among them, and only the userowner_idpoints at may readsummary. What a caller is sent has one shape for everyone, and a value passed through a variable or another type keeps its field's rules.- next
- Leave
summaryout ofSummaryView(agent) - rule
- policy.
output_ field_ unproved - help
- https://jadpo.
dev/docs/errors/policy. output_ field_ unproved
What a write stores is read later, from any request, by everyone who may read the field it is stored in. So a value is stored only where all of them may read it. Here the reviewer copies the appraisal into the summary, which the note's owner may read too:
type Handle = Text { max_length: 40}entity User { id: Uuid handle: Handle { unique: true }}principal { user: { subject: User.handle }}type NoteText = Text { max_length: 200}entity Note { id: Uuid owner_id: User.id { on_delete: OnDelete.cascade } reviewer_id: User.id { on_delete: OnDelete.cascade } summary: NoteText appraisal: NoteText { policy: { read: [reviewer_id] } } policy: { read: [owner_id, reviewer_id] update: [reviewer_id] }}action publish_appraisal(note_id: Note.id) fails NoteNotFound -> Note { var note: Note = attempt Note.get(note_id) return Note.update(note) { set: { summary: note.appraisal } }}
error publish_appraisal stores Note.appraisal in Note.summary, which not everyone who may read summary may read
--> main.jadpo:47:22
|47 | summary: note.appraisal | ^^^^^^^^^^^^^^ from `Note.appraisal`
- reason
- Everyone who may read
Note.summary, the userowner_idpoints at among them, reads what is stored there, from any request, and only the userreviewer_idpoints at may readappraisal. A value stored in a field keeps its own field's rules, as one sent in an output does, so it is stored only where everyone who may read it may read the value. - next
- Leave
appraisalout ofsummary, or store it in a field only those who may readappraisalmay read (agent) - rule
- policy.
output_ field_ unproved - help
- https://jadpo.
dev/docs/errors/policy. output_ field_ unproved
A copy between fields the same callers read stays allowed. Here the reviewer's draft, which only the reviewer may read, becomes the appraisal, which only the reviewer may read too. A copy into a record that refers to the one it came from, or into one of the same tenant, is allowed in the same way when everyone who may read the field may read the value there:
type Handle = Text { max_length: 40}entity User { id: Uuid handle: Handle { unique: true }}principal { user: { subject: User.handle }}type NoteText = Text { max_length: 200}entity Note { id: Uuid owner_id: User.id { on_delete: OnDelete.cascade } reviewer_id: User.id { on_delete: OnDelete.cascade } summary: NoteText draft: NoteText { policy: { read: [reviewer_id] } } appraisal: NoteText { policy: { read: [reviewer_id] } } policy: { read: [owner_id, reviewer_id] update: [reviewer_id] }}action finish_review(note_id: Note.id) fails NoteNotFound -> Note { var note: Note = attempt Note.get(note_id) return Note.update(note) { set: { appraisal: note.draft } }}
A subscriber's action stores what its event carries, and the event
carries what every emit of it put there. Here one of the two actions
that share a note puts its appraisal, which only the reviewer may read,
in the event, and the bulletin the subscriber posts is read by anyone
signed in:
type Handle = Text { max_length: 40}entity User { id: Uuid handle: Handle { unique: true }}principal { user: { subject: User.handle }}type NoteText = Text { max_length: 200}entity Note { id: Uuid reviewer_id: User.id { on_delete: OnDelete.cascade } circulated: Bool summary: NoteText appraisal: NoteText { policy: { read: [reviewer_id] } } policy: { read: [Access.authenticated] update: [reviewer_id] }}entity Bulletin { id: Uuid blurb: NoteText policy: { create: [Access.system] read: [Access.authenticated] }}type NoteCirculated = Event { note_id: Note.id blurb: NoteText}action circulate_summary(note_id: Note.id) fails NoteNotFound -> Unit { var note: Note = attempt Note.update(note_id) { set: { circulated: true } } emit NoteCirculated { note_id: note.id blurb: note.summary }}action circulate_appraisal(note_id: Note.id) fails NoteNotFound -> Unit { var note: Note = attempt Note.update(note_id) { set: { circulated: true } } emit NoteCirculated { note_id: note.id blurb: note.appraisal }}action post_bulletin(blurb: NoteText) -> Unit { var bulletin: Bulletin = Bulletin.create { set: { blurb: blurb } } assert bulletin.blurb == blurb}subscriber post_circulated { on: NoteCirculated run: post_bulletin(event.blurb)}
error post_circulated passes Note.appraisal to post_bulletin, which stores it in Bulletin.blurb, though not everyone who may read blurb may read it
--> main.jadpo:91:24
|91 | run: post_bulletin(event.blurb) | ^^^^^^^^^^^ an `emit` filled this from `Note.appraisal`
- reason
post_bulletinstores what it is given here inBulletin.blurb, and everyone who may readBulletin.blurb, anyone signed in among them, reads what is stored there, from any request, and only the userreviewer_idpoints at may readappraisal. An event carries what eachemitof it put there away from the record it came from, so its subscriber stores it only where everyone who may read it may readappraisalof anyNote.- next
- Pass the event's ids and have
post_bulletinread what it stores, or giveblurb'sread:only those who may readappraisal(agent) - rule
- policy.
output_ field_ unproved - help
- https://jadpo.
dev/docs/errors/policy. output_ field_ unproved
With only the summary, which anyone signed in may read, in the event, the bulletin may hold it. A value an event carries has left its record, which may change before the delivery. So it is proved as a value from any record of its entity, or from a record of the event's tenant when the event's tenant field came from that record, since the subscriber is limited to that tenant (Subscribers):
type Handle = Text { max_length: 40}entity User { id: Uuid handle: Handle { unique: true }}principal { user: { subject: User.handle }}type NoteText = Text { max_length: 200}entity Note { id: Uuid reviewer_id: User.id { on_delete: OnDelete.cascade } circulated: Bool summary: NoteText appraisal: NoteText { policy: { read: [reviewer_id] } } policy: { read: [Access.authenticated] update: [reviewer_id] }}entity Bulletin { id: Uuid blurb: NoteText policy: { create: [Access.system] read: [Access.authenticated] }}type NoteCirculated = Event { note_id: Note.id blurb: NoteText}action circulate_summary(note_id: Note.id) fails NoteNotFound -> Unit { var note: Note = attempt Note.update(note_id) { set: { circulated: true } } emit NoteCirculated { note_id: note.id blurb: note.summary }}action post_bulletin(blurb: NoteText) -> Unit { var bulletin: Bulletin = Bulletin.create { set: { blurb: blurb } } assert bulletin.blurb == blurb}subscriber post_circulated { on: NoteCirculated run: post_bulletin(event.blurb)}
A webhook's reply action runs as the system, which finds every record
its grants name, and its reply goes back to whoever sent the webhook. So
a reply that counts or tests for records only the system may read shows
whether they are there, and so does whether it fails. Each is proved as
an output to Access.public:
domains/support/actions/answer_call.jadpo:
use support/entities { PhoneLog }use support/queries { recent_calls }use shared/providers/acme_voice/// Answers a call Acme Voice rings through, welcoming a caller back.action answer_call(event: AcmeVoice.IncomingCall) -> AcmeVoice.CallReply { var earlier: List<PhoneLog> = recent_calls() var mut said: Text = "Thanks for calling." if (list.count(earlier) > 0) { said = "Welcome back." } return AcmeVoice.CallReply { spoken: said hang_up: false }}
error answer_call's reply shows whether a PhoneLog is there, which not everyone may read
--> domains/support/actions/answer_call.jadpo:12:12
|12 | return AcmeVoice.CallReply { | ^^^^^^^^^^^^^^^^^^^^^ decided by whether a `PhoneLog` is there
- reason
- A webhook's reply action answers whoever sent the webhook, with its reply or a failure, so each is an output to
Access.public, and only schedules and subscribers may read aPhoneLog. Which reply is sent shows whether the records it counted or tested are there, since the action runs as the system and finds every one. - next
- Decide the reply without counting or testing for a
PhoneLog(agent) - rule
- policy.
output_ field_ unproved - help
- https://jadpo.
dev/docs/errors/policy. output_ field_ unproved
How to fix it
The question is whether everyone who may run find_note should see the
appraisal. Each further case above is fixed the same way, by what its
next: line says: send, test, find by or store only what every caller
may read.
If the callers who may not read the field don't need it, leave it out of the output:
type NoteView = Output { pick: [Note.id, Note.text]}
find_note sends a note's id and text to everyone Note's read:
lets in, owners and reviewers alike, and the appraisal stays the
reviewer's.
If everyone who may read a note should read its appraisal, remove the
field's own policy:, so it follows Note's grants:
text: NoteText appraisal: NoteText
The note's owner may now read its appraisal, and find_note sends it to
them. Removing a field's restriction widens who may read it, so in an app
already on its default branch the change also needs a person's acceptance
(Protecting policy).
Related
Markdown for agents: /docs/errors/policy.output_field_unproved.md.Built from Jadpo main at aee2e8f.