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
80 changes: 80 additions & 0 deletions .trajectories/completed/2026-09/traj_qtxkeaa5cphq.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
{
"id": "traj_qtxkeaa5cphq",
"version": 1,
"task": {
"title": "Repair ACL pathless provider deletion reconciliation",
"source": {
"system": "plain",
"id": "Relayfile ACL-423"
}
},
"status": "completed",
"startedAt": "2026-09-08T15:41:42.556Z",
"completedAt": "2026-09-08T15:47:16.049Z",
"agents": [
{
"name": "default",
"role": "lead",
"joinedAt": "2026-09-08T15:41:47.673Z"
}
],
"chapters": [
{
"id": "chap_bevz9521bn7k",
"title": "Work",
"agentName": "default",
"startedAt": "2026-09-08T15:41:47.673Z",
"endedAt": "2026-09-08T15:47:16.049Z",
"events": [
{
"ts": 1788882107674,
"type": "decision",
"content": "Use a pathless sync.reconcile control event for unresolved provider deletes: Use a pathless sync.reconcile control event for unresolved provider deletes",
"raw": {
"question": "Use a pathless sync.reconcile control event for unresolved provider deletes",
"chosen": "Use a pathless sync.reconcile control event for unresolved provider deletes",
"alternatives": [],
"reasoning": "Never guess or disclose hidden paths; filtered mounts receive an authoritative full reconciliation in the same sync cycle while malformed empty-path file.deleted events remain fail-closed."
},
"significance": "high"
},
{
"ts": 1788882435983,
"type": "reflection",
"content": "Pathless provider deletion now emits only a durable sync.reconcile control when no ACL-backed path can be named; mounts persist the prompt, reconcile authoritatively in-cycle, and filtered consumers receive no path. Focused/full serialized Go and package gates are green; parallel full Go and mountsync race expose inherited test-environment races.",
"raw": {
"focalPoints": [
"pathless deletion",
"ACL fail-closed",
"restart durability",
"inherited races"
],
"adjustments": "Added durable PendingFullReconcile state and consumer path-filter exceptions",
"confidence": 0.92
},
"significance": "high",
"tags": [
"focal:pathless deletion",
"focal:ACL fail-closed",
"focal:restart durability",
"focal:inherited races",
"confidence:0.92"
]
}
]
}
],
"retrospective": {
"summary": "Repaired Relayfile ACL pathless provider deletion handling with sync.reconcile control events, durable prompt reconciliation, no-path fail-closed filtering, SDK/file-observer parity, and regression tests.",
"approach": "Standard approach",
"confidence": 0.92
},
"commits": [],
"filesChanged": [],
"projectId": "/private/tmp/relayfile-acl423-fix-0908",
"tags": [],
"_trace": {
"startRef": "45820e7a50002a19245f2c5984df996bd650d455",
"endRef": "45820e7a50002a19245f2c5984df996bd650d455"
}
}
33 changes: 33 additions & 0 deletions .trajectories/completed/2026-09/traj_qtxkeaa5cphq.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Trajectory: Repair ACL pathless provider deletion reconciliation

> **Status:** ✅ Completed
> **Task:** Relayfile ACL-423
> **Confidence:** 92%
> **Started:** September 8, 2026 at 05:41 PM
> **Completed:** September 8, 2026 at 05:47 PM

---

## Summary

Repaired Relayfile ACL pathless provider deletion handling with sync.reconcile control events, durable prompt reconciliation, no-path fail-closed filtering, SDK/file-observer parity, and regression tests.

**Approach:** Standard approach

---

## Key Decisions

### Use a pathless sync.reconcile control event for unresolved provider deletes
- **Chose:** Use a pathless sync.reconcile control event for unresolved provider deletes
- **Reasoning:** Never guess or disclose hidden paths; filtered mounts receive an authoritative full reconciliation in the same sync cycle while malformed empty-path file.deleted events remain fail-closed.

---

## Chapters

### 1. Work
*Agent: default*

- Use a pathless sync.reconcile control event for unresolved provider deletes: Use a pathless sync.reconcile control event for unresolved provider deletes
- Pathless provider deletion now emits only a durable sync.reconcile control when no ACL-backed path can be named; mounts persist the prompt, reconcile authoritatively in-cycle, and filtered consumers receive no path. Focused/full serialized Go and package gates are green; parallel full Go and mountsync race expose inherited test-environment races.
25 changes: 23 additions & 2 deletions .trajectories/index.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"version": 1,
"lastUpdated": "2026-08-27T20:20:27.504Z",
"lastUpdated": "2026-09-08T18:17:10.919Z",
"trajectories": {
"traj_4pvrlmqfnzng": {
"title": "Review PR #278 in AgentWorkforce/relayfile",
Expand Down Expand Up @@ -204,6 +204,27 @@
"startedAt": "2026-08-27T20:19:59.085Z",
"completedAt": "2026-08-27T20:20:27.355Z",
"path": ".trajectories/completed/2026-08/traj_7n063n1f3wai.json"
},
"traj_qtxkeaa5cphq": {
"title": "Repair ACL pathless provider deletion reconciliation",
"status": "completed",
"startedAt": "2026-09-08T15:41:42.556Z",
"completedAt": "2026-09-08T15:47:16.049Z",
"path": ".trajectories/completed/2026-09/traj_qtxkeaa5cphq.json"
},
"traj_l5ctfmv1nb5j": {
"title": "Repair ACL-safe provider upserts and healthy realtime reconciliation",
"status": "completed",
"startedAt": "2026-09-08T18:05:42.152Z",
"completedAt": "2026-09-08T18:12:02.896Z",
"path": "/private/tmp/relayfile-acl423-fix-0908/.trajectories/completed/2026-09/traj_l5ctfmv1nb5j.json"
},
"traj_f5e9ezrw8o6g": {
"title": "Fail closed on ambiguous provider-object pathless upserts",
"status": "completed",
"startedAt": "2026-09-08T18:17:10.022Z",
"completedAt": "2026-09-08T18:17:10.768Z",
"path": "/private/tmp/relayfile-acl423-fix-0908/.trajectories/completed/2026-09/traj_f5e9ezrw8o6g.json"
}
}
}
}
2 changes: 1 addition & 1 deletion docs/bulk-export-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ To prevent missing events between subscribe and the first live event:
}
```

Event types: `file.created`, `file.updated`, `file.deleted`, `dir.created`, `dir.deleted`, `sync.error`, `sync.ignored`, `sync.suppressed`, `sync.stale`, `writeback.failed`, `writeback.succeeded`.
Event types: `file.created`, `file.updated`, `file.deleted`, `dir.created`, `dir.deleted`, `sync.error`, `sync.ignored`, `sync.suppressed`, `sync.stale`, `sync.reconcile`, `writeback.failed`, `writeback.succeeded`. `sync.reconcile` is a pathless provider-sync control event; mounts must perform an authoritative reconciliation and must not infer a path from it.

**Pong (response to client ping):**

Expand Down
105 changes: 98 additions & 7 deletions internal/httpapi/acl.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,9 @@ func parsePermissionRule(raw string) *ParsedPermissionRule {
// aclAgentNamePattern allows alphanumerics, hyphens, underscores, and dots.
var aclAgentNamePattern = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$`)

// aclScopePattern allows scope values like "fs:read", "sync:trigger".
// aclScopePattern allows unscoped capability/tag values like "fs:read",
// "sync:trigger", and "finance". Path-bearing filesystem scopes are
// validated separately by isValidACLFilesystemScope.
var aclScopePattern = regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9]*(?::[a-zA-Z][a-zA-Z0-9]*)*$`)

// aclWorkspacePattern allows workspace IDs like "ws_123" or UUIDs.
Expand All @@ -78,22 +80,93 @@ func isValidACLRuleValue(kind, value string) bool {
case "agent":
return aclAgentNamePattern.MatchString(value)
case "scope":
return aclScopePattern.MatchString(value)
return aclScopePattern.MatchString(value) || isValidACLFilesystemScope(value)
case "workspace":
return aclWorkspacePattern.MatchString(value)
default:
return false
}
}

// filePermissionAllows evaluates ACL rules against agent claims.
type parsedACLFilesystemScope struct {
action string
path string
}

// parseACLFilesystemScope recognizes both the RelayAuth four-segment scope
// vocabulary and Relayfile's legacy workspace-tag vocabulary. The latter is
// still present in durable ACL markers created by Cloud, but no longer needs
// to be carried literally by delegated tokens.
func parseACLFilesystemScope(scope string) (*parsedACLFilesystemScope, bool) {
segments := strings.SplitN(scope, ":", 4)
if len(segments) == 2 && segments[0] == "fs" {
if !isACLFilesystemAction(segments[1]) {
return nil, false
}
return &parsedACLFilesystemScope{action: segments[1], path: "*"}, true
}
if len(segments) != 4 {
return nil, false
}

plane := segments[0]
resource := segments[1]
action := segments[2]
path := segments[3]
if strings.TrimSpace(path) == "" {
return nil, false
}

switch plane {
case "relayfile", "*":
if resource != "fs" && resource != "*" {
return nil, false
}
case "workspace":
if !aclAgentNamePattern.MatchString(resource) {
return nil, false
}
default:
return nil, false
}

if !isACLFilesystemAction(action) {
return nil, false
}
return &parsedACLFilesystemScope{action: action, path: path}, true
}

func isACLFilesystemAction(action string) bool {
return action == "read" || action == "write" || action == "manage" || action == "*"
}

func isValidACLFilesystemScope(scope string) bool {
if strings.TrimSpace(scope) != scope {
return false
}
parsed, ok := parseACLFilesystemScope(scope)
if !ok {
return false
}
return scopePathValid(parsed.path)
}

// filePermissionAllows evaluates ACL rules against agent claims for one
// filesystem action and path. Scope rules are semantic: a durable rule such
// as relayfile:fs:write:/protected/* matches a delegated token carrying the
// broader relayfile:fs:write:* grant without requiring the rule itself to be
// copied into the token.
// Returns true if access is allowed.
func filePermissionAllows(permissions []string, workspaceID string, claims *tokenClaims) bool {
func filePermissionAllows(permissions []string, workspaceID string, claims *tokenClaims, requiredAction, requestedPath string) bool {
if len(permissions) == 0 {
// No ACL policy in effect — allow access.
return true
}

if requestedPath != "" {
requestedPath = normalizeACLPath(requestedPath)
}

enforceableRuleSeen := false
allowMatch := false
for _, raw := range permissions {
Expand All @@ -110,9 +183,7 @@ func filePermissionAllows(permissions []string, workspaceID string, claims *toke
case "public":
match = true
case "scope":
if claims != nil {
_, match = claims.Scopes[rule.Value]
}
match = aclScopeRuleMatches(rule.Value, claims, requiredAction, requestedPath)
case "agent":
match = claims != nil && claims.AgentName == rule.Value
case "workspace":
Expand All @@ -137,6 +208,26 @@ func filePermissionAllows(permissions []string, workspaceID string, claims *toke
return !enforceableRuleSeen
}

func aclScopeRuleMatches(scope string, claims *tokenClaims, requiredAction, requestedPath string) bool {
if claims == nil {
return false
}

parsed, filesystemScope := parseACLFilesystemScope(scope)
if !filesystemScope {
_, exactMatch := claims.Scopes[scope]
return exactMatch
}
if !scopeActionMatches(parsed.action, requiredAction) {
return false
}
if parsed.path != "*" && !scopePathMatches(parsed.path, requestedPath) {
Comment thread
khaliqgant marked this conversation as resolved.
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
return false
}

return scopeMatchesPath(claims.Scopes, "fs:"+requiredAction, requestedPath)
}

// resolveFilePermissions walks ancestor dirs to collect ACL rules.
// store is an interface that can read files from the workspace.
func resolveFilePermissions(getFile func(path string) ([]byte, error), path string) []string {
Expand Down
Loading
Loading