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
10 changes: 10 additions & 0 deletions internal/cloud/cloudstore/cloudstore.go
Original file line number Diff line number Diff line change
Expand Up @@ -781,6 +781,16 @@ func (cs *CloudStore) migrate(ctx context.Context) error {
claimed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (session_id, source_inbox_id)
)`,
`CREATE TABLE IF NOT EXISTS cloud_prompt_source_attestations (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
session_id TEXT NOT NULL,
source_inbox_id TEXT NOT NULL,
sync_id TEXT NOT NULL,
owner_project TEXT NOT NULL,
prompt_project TEXT NOT NULL,
actor_id TEXT NOT NULL,
attested_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)`,
`CREATE TABLE IF NOT EXISTS cloud_project_sessions (
project_name TEXT NOT NULL,
session_id TEXT NOT NULL,
Expand Down
59 changes: 59 additions & 0 deletions internal/cloud/cloudstore/prompt_source_attestation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package cloudstore

import (
"context"
"database/sql"
"errors"
"fmt"
"strings"
"time"
)

var ErrPromptSourceAttestationUnbound = errors.New("cloudstore: prompt source attestation requires exact session authority and prompt pair claim")

// PromptSourceAttestation is an independent, append-only record of an explicit
// human source assertion; it does not alter registration or claim audit history.
type PromptSourceAttestation struct {
ID int64
SessionID string
SourceInboxID string
SyncID string
OwnerProject string
PromptProject string
ActorID string
AttestedAt time.Time
}

// AttestPromptSource records an already-authorized human assertion. Storage
// does not check grants: the future authenticated server route must verify the
// actor's current grants to both owner and prompt projects before calling it.
func (cs *CloudStore) AttestPromptSource(ctx context.Context, sessionID, sourceInboxID, syncID, ownerProject, promptProject, actorID string) (*PromptSourceAttestation, error) {
if cs == nil || cs.db == nil {
return nil, fmt.Errorf("cloudstore: not initialized")
}
values := []*string{&sessionID, &sourceInboxID, &syncID, &ownerProject, &promptProject, &actorID}
for _, value := range values {
*value = strings.TrimSpace(*value)
if *value == "" {
return nil, fmt.Errorf("cloudstore: all attestation fields are required")
}
}
const query = `INSERT INTO cloud_prompt_source_attestations
(session_id, source_inbox_id, sync_id, owner_project, prompt_project, actor_id)
SELECT a.session_id, c.source_inbox_id, c.sync_id, a.owner_project, c.prompt_project, $6
FROM cloud_session_authority a
JOIN cloud_prompt_pair_claims c ON c.session_id = a.session_id
WHERE a.session_id = $1 AND c.source_inbox_id = $2 AND c.sync_id = $3
AND a.owner_project = $4 AND c.prompt_project = $5
RETURNING id, session_id, source_inbox_id, sync_id, owner_project, prompt_project, actor_id, attested_at`
var record PromptSourceAttestation
err := cs.db.QueryRowContext(ctx, query, sessionID, sourceInboxID, syncID, ownerProject, promptProject, actorID).
Scan(&record.ID, &record.SessionID, &record.SourceInboxID, &record.SyncID, &record.OwnerProject, &record.PromptProject, &record.ActorID, &record.AttestedAt)
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrPromptSourceAttestationUnbound
}
if err != nil {
return nil, fmt.Errorf("cloudstore: attest prompt source: %w", err)
}
return &record, nil
}
74 changes: 74 additions & 0 deletions internal/cloud/cloudstore/prompt_source_attestation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package cloudstore

import (
"context"
"errors"
"testing"
)

func TestPromptSourceAttestationExactAuthorityAndAppendOnlyAudit(t *testing.T) {
cs := openIsolatedCloudStore(t)
ctx := context.Background()
if _, err := cs.db.ExecContext(ctx, `INSERT INTO cloud_project_sessions (project_name, session_id) VALUES ('owner', 'unregistered')`); err != nil {
t.Fatal(err)
}
if _, err := cs.AttestPromptSource(ctx, "unregistered", "inbox", "sync", "owner", "prompt", "human"); !errors.Is(err, ErrPromptSourceAttestationUnbound) {
t.Fatalf("indexed session conferred authority: %v", err)
}
if err := cs.RegisterSessionAuthority(ctx, "session", "owner", "registrar"); err != nil {
t.Fatal(err)
}
if _, err := cs.AttestPromptSource(ctx, "session", "inbox", "sync", "owner", "prompt", "human"); !errors.Is(err, ErrPromptSourceAttestationUnbound) {
t.Fatalf("unclaimed pair conferred authority: %v", err)
}
if err := cs.ClaimPromptPair(ctx, "session", "inbox", "sync", "prompt", "claimer"); err != nil {
t.Fatal(err)
}
before, err := cs.GetPromptPairClaim(ctx, "session", "inbox")
if err != nil {
t.Fatal(err)
}
for _, tc := range []struct {
name string
args [6]string
}{
{"wrong session", [6]string{"other", "inbox", "sync", "owner", "prompt", "human"}},
{"wrong inbox", [6]string{"session", "other", "sync", "owner", "prompt", "human"}},
{"wrong sync", [6]string{"session", "inbox", "other", "owner", "prompt", "human"}},
{"wrong owner", [6]string{"session", "inbox", "sync", "prompt", "prompt", "human"}},
{"wrong prompt", [6]string{"session", "inbox", "sync", "owner", "owner", "human"}},
} {
t.Run(tc.name, func(t *testing.T) {
a := tc.args
if _, err := cs.AttestPromptSource(ctx, a[0], a[1], a[2], a[3], a[4], a[5]); !errors.Is(err, ErrPromptSourceAttestationUnbound) {
t.Fatalf("accepted mismatch: %v", err)
}
})
}
for i := 0; i < 6; i++ {
a := [6]string{"session", "inbox", "sync", "owner", "prompt", "human"}
a[i] = " "
if _, err := cs.AttestPromptSource(ctx, a[0], a[1], a[2], a[3], a[4], a[5]); err == nil {
t.Fatalf("accepted blank field %d", i)
}
}
first, err := cs.AttestPromptSource(ctx, "session", "inbox", "sync", "owner", "prompt", "human-one")
if err != nil {
t.Fatal(err)
}
second, err := cs.AttestPromptSource(ctx, "session", "inbox", "sync", "owner", "prompt", "human-two")
if err != nil {
t.Fatal(err)
}
if first.ID == second.ID || first.AttestedAt.IsZero() || second.AttestedAt.IsZero() || first.ActorID != "human-one" || second.ActorID != "human-two" {
t.Fatalf("audit records not independent: %+v %+v", first, second)
}
var count int
if err := cs.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM cloud_prompt_source_attestations`).Scan(&count); err != nil || count != 2 {
t.Fatalf("count=%d err=%v", count, err)
}
after, err := cs.GetPromptPairClaim(ctx, "session", "inbox")
if err != nil || *before != *after {
t.Fatalf("claim audit changed: %+v %+v %v", before, after, err)
}
}
Loading