Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
97 commits
Select commit Hold shift + click to select a range
3ba1d8f
Honor chat-requested models for task workers and new conversations (#…
santoshkumarradha Oct 25, 2026
c4bd257
Answer and verify the four skill capability questions (#1938)
santoshkumarradha Oct 25, 2026
0e10604
Make All show the current conversation tabs (#1939)
santoshkumarradha Oct 25, 2026
7f5da93
Run the same program when connecting to another machine (#1940)
santoshkumarradha Oct 25, 2026
9ada680
Add linked project knowledge vault to Memory (#1943)
santoshkumarradha Oct 25, 2026
5fb1ddf
docs: organize product, developer, and contributor knowledge (#1945)
santoshkumarradha Oct 25, 2026
f6dc162
Fix chat startup, blank tab switches, and delayed team workers (#1946)
santoshkumarradha Oct 25, 2026
a08b936
fix(tui): keep tabs usable while opening a chat (#1947)
santoshkumarradha Oct 26, 2026
1d028bf
Keep startup and resume responsive during storage and engine delays (…
santoshkumarradha Oct 26, 2026
ed8bf75
fix(tui): keep team navigation responsive during first worker open (#…
santoshkumarradha Oct 26, 2026
e88e49a
Make Add another device clickable with an editable pairing field (#1949)
santoshkumarradha Oct 26, 2026
808c90c
Relay: coordination as a Raft group, and joined computers can lead (#…
AbirAbbas Oct 26, 2026
f0cf232
Bump embedded furrow to v0.3.3 (fast store open) (#1962)
santoshkumarradha Oct 26, 2026
b30e0b9
perf(engine): keep furrow detection off the session build path (#1954)
santoshkumarradha Oct 26, 2026
334cab9
feat(knowledge): replace Python with native background builds (#1966)
santoshkumarradha Oct 26, 2026
4b03be9
Make the home add-device card clearer and clickable (#1968)
santoshkumarradha Oct 26, 2026
08b922b
fix(session): a swept fork stays retired (#1969)
santoshkumarradha Oct 26, 2026
9e34209
Two computers: continue a moved chat, clean leader quit, removed-devi…
AbirAbbas Oct 26, 2026
f961fbc
perf(enginehost): a retiring host gives the workspace up before it sa…
santoshkumarradha Oct 26, 2026
6ef871a
Bump embedded furrow to v0.3.4 (prune abandoned capture plans) (#1974)
santoshkumarradha Oct 26, 2026
cf282dd
fix(cellstore): never capture a folder that holds the home folder (#1…
santoshkumarradha Oct 26, 2026
4326eb3
perf: an idle client stops reopening the vault per key and retrying f…
santoshkumarradha Oct 26, 2026
d0ff650
Build reusable knowledge guides from saved history (#1975)
santoshkumarradha Oct 26, 2026
ff5543f
Private task copies (opt-in), frame hints through the Raft relay, and…
santoshkumarradha Oct 26, 2026
644a38e
Two-computer relay: fast take-back, moved task pages, honest saving, …
AbirAbbas Oct 27, 2026
0447a05
Maintain coherent knowledge guides and reuse validated resolver work …
santoshkumarradha Oct 27, 2026
62ea76d
Reuse unaffected knowledge evidence groups after corrections (#2000)
santoshkumarradha Oct 27, 2026
cc1aaa8
Quitting right after work saves quietly and never branches bookkeepin…
AbirAbbas Oct 27, 2026
3bf6072
fix(sync): never copy an older chat that works in the home folder (#2…
santoshkumarradha Oct 27, 2026
af03038
fix: honor explicit fallbacks and preserve finished shell calls (#2020)
AbirAbbas Oct 28, 2026
66fa77e
fix: reject malformed write payloads before changing files (#2028)
santoshkumarradha Oct 29, 2026
b8f62f6
cellstore: background seal notices go to the log, not through the cha…
santoshkumarradha Oct 29, 2026
9508f1e
fix: --host over ssh no longer evicts a session host started with COD…
AbirAbbas Oct 30, 2026
ce06d29
feat(settings): a Devices tab — add, rename and remove computers from…
AbirAbbas Oct 30, 2026
34ced44
Reach a paired computer through the relay sync uses (#2073)
santoshkumarradha Oct 30, 2026
9a03226
Never lock a person out of their own chat on an older sync service (#…
santoshkumarradha Oct 30, 2026
6206dbc
fix(relay): harden the machine doors #2073 put on the hosted relay (#…
AbirAbbas Oct 30, 2026
f128ec3
fix: make dev's Full check green (promotion tests, archived relay cli…
AbirAbbas Oct 30, 2026
f3e4f6f
Continue a chat from another computer every time, and let doctor repa…
AbirAbbas Oct 30, 2026
72a2792
One relay per release channel, deployed by CI when relay code changes…
AbirAbbas Oct 30, 2026
70c5c62
Sync off keeps every chat, and a copied older chat can no longer be w…
AbirAbbas Oct 31, 2026
152134c
codeaf devbox: an always-on computer on your own Railway account (#2081)
AbirAbbas Oct 31, 2026
9638112
Fix what the Mac tester found: a deleted home is never rewritten, mac…
AbirAbbas Oct 31, 2026
9be6706
A displaced window keeps its unsent turns, and a taken chat can be ta…
AbirAbbas Oct 31, 2026
b42fdf1
teams: opt-in team sharing between paired computers (#2080)
AbirAbbas Oct 31, 2026
4e42800
Continue the chat a person picks; keep the retry card; honest waits o…
AbirAbbas Oct 31, 2026
23f7edd
Run a task on another paired computer while the chat stays here (#2085)
AbirAbbas Oct 31, 2026
778ee15
test: remote-task session fixtures pass on macOS temporary folders (#…
AbirAbbas Oct 31, 2026
20eb695
Pairing, devices and sync connection: say the true thing, and ask bef…
AbirAbbas Oct 31, 2026
cbd3649
ci: remove the daily Release downloads workflow (#2193)
AbirAbbas Nov 2, 2026
7e1b9a6
docs: the tasks manual points task.parallel at Tasks, not Spending (#…
7vignesh Nov 2, 2026
1fe2e8c
relay: the production config keeps prod's CAF_LIMITS (#2214)
AbirAbbas Nov 2, 2026
33f438e
Team sharing on by default for paired computers, made safe (#2095)
AbirAbbas Nov 3, 2026
05499b4
Bump embedded furrow to v0.3.6 (materialize snapshots containing sock…
AbirAbbas Nov 3, 2026
067d619
Merge origin/dev into app/v3 with recorded ancestry
santoshkumarradha Nov 3, 2026
2531422
Merge latest origin/app/v3 after dev ancestry repair
santoshkumarradha Nov 3, 2026
c4a0834
Document dev ancestry repair for codeaf
santoshkumarradha Nov 3, 2026
db2c06f
Merge newest origin/dev into app/v3
santoshkumarradha Nov 3, 2026
3382362
Validate newest dev integration with app/v3 behavior
santoshkumarradha Nov 3, 2026
d090b37
Merge remote-tracking branch 'origin/app/v3' into HEAD
santoshkumarradha Nov 3, 2026
a94d7e2
Correct the team sharing default in the manual and PERF.md
santoshkumarradha Nov 3, 2026
8ad21b3
Adapt team relay regression fixtures to SQLite and opt-in sharing
santoshkumarradha Nov 3, 2026
1769c58
Merge app/v3 into dev merge
santoshkumarradha Nov 3, 2026
93755da
Merge newest app/v3 under Factory owner rule
santoshkumarradha Nov 3, 2026
5484a2c
Restore dev sharing defaults and ordinary file memo fallback
santoshkumarradha Nov 3, 2026
d08bca0
Require confirmed replacement for used solo computers
santoshkumarradha Nov 3, 2026
10d456a
Limit unsupported-effort repair to opted-in task clients
santoshkumarradha Nov 3, 2026
93d4b7f
Restore dev ownership expiry and exact-fence consent
santoshkumarradha Nov 3, 2026
8b88f75
fix: retain Factory client effort recovery
santoshkumarradha Nov 3, 2026
78011b1
fix: complete same-account replacement cleanup
santoshkumarradha Nov 3, 2026
1f5060e
fix: preserve unreadable profile spending authority
santoshkumarradha Nov 3, 2026
c202693
fix: distinguish Factory work from available tools
santoshkumarradha Nov 3, 2026
1960297
Merge latest app/v3 for Factory owner-rule follow-up
santoshkumarradha Nov 3, 2026
027c629
Scope Factory reasoning recovery to live calls and align pairing with…
santoshkumarradha Nov 3, 2026
056630e
Merge newest app/v3 Factory job touch tracking
santoshkumarradha Nov 3, 2026
8295923
Use one Factory AI classification for billing and refusal recovery
santoshkumarradha Nov 3, 2026
a6d0989
fix: recover effort for job-owned Factory conversations
santoshkumarradha Nov 3, 2026
4474fb6
fix: prompt on unreadable ordinary profile policy
santoshkumarradha Nov 3, 2026
a042b31
fix: require human approval when profile rules cannot be read
santoshkumarradha Nov 3, 2026
b46824a
Merge app/v3 into dev merge
santoshkumarradha Nov 3, 2026
2421f5a
Manual names the two kinds of work an unreadable profile does not pro…
santoshkumarradha Nov 3, 2026
f4db1f4
Amortize hosted relay authorization and defer peer formation (#2162)
santoshkumarradha Nov 3, 2026
36d72a6
fix: restore automatic retries for ordinary memory builds
santoshkumarradha Nov 4, 2026
9d5573e
Merge newest dev into the dev merge
santoshkumarradha Nov 4, 2026
09d1d64
fix: bound ordinary memory build retries across restarts
santoshkumarradha Nov 4, 2026
758d98a
Merge newest app/v3 into the dev merge
santoshkumarradha Nov 4, 2026
b788f30
fix: preserve local chats when a removed computer re-pairs
santoshkumarradha Nov 4, 2026
b62309a
fix: keep empty memory startup joinable
santoshkumarradha Nov 4, 2026
90acb78
merge: bring app/v3 0982bf6b41 into the dev merge
santoshkumarradha Nov 4, 2026
414e036
merge: bring app/v3 419631c50c into the dev merge
santoshkumarradha Nov 4, 2026
e256d4f
merge: bring app/v3 57a8593d9d into the dev merge
santoshkumarradha Nov 4, 2026
1950ba0
merge: bring app/v3 c50abac31a into the dev merge
santoshkumarradha Nov 4, 2026
aa11451
merge: bring app/v3 59cdf222b6 into the dev merge
santoshkumarradha Nov 4, 2026
8fc146c
test: pair before saving the key in the home-sharing tests
santoshkumarradha Nov 4, 2026
a32c3f0
Merge remote-tracking branch 'origin/app/v3' into HEAD
santoshkumarradha Nov 4, 2026
dafd6b6
Merge remote-tracking branch 'origin/app/v3' into HEAD
santoshkumarradha Nov 4, 2026
87c7f49
CheckLease waits for the branch when the hold is already lost
santoshkumarradha Nov 4, 2026
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
33 changes: 0 additions & 33 deletions .github/workflows/release-downloads.yml

This file was deleted.

13 changes: 8 additions & 5 deletions PERF.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,16 +193,16 @@ same commit; derive repeated values from constants.

## Paired team history

These sharing budgets apply only when **share teams** is on in Settings → Devices.
Sharing is off by default on each computer; off rounds run no team carriers,
These sharing budgets apply only when **share teams** is on in Settings → General.
Sharing is on by default for paired computers; off rounds run no team carriers,
identity discovery, mirror writes or ownership lookups and retain existing polling.

`teams.TrafficSyncLines` limits each writing computer's window to 32 lines per
team, and `teams.TrafficSyncBytes` limits encoded lines to 16 KiB per team.
`teams.TrafficSyncTextBytes` shortens text beyond 2 KiB at a rune boundary.
`teams.TrafficSyncDeviceBytes` caps each device's encoded Traffic share at 64 KiB
across teams, newest local activity first. `teams.TrafficSyncEvery` permits a
publication at most once every 30 seconds; team record edits are not delayed.
publication at most once every 2 minutes; team record edits are not delayed.
Capture reads the bounded backward tail across the current and rotated logs. Received windows replace a
separate display mirror and are never republished. An unchanged round on 500
chats and 5 teams performs zero world scans and opens zero cell metadata files;
Expand All @@ -212,8 +212,11 @@ compare deterministic values, and verify quiet rounds leave the vault unchanged.
Mirror filtering makes zero directory requests per vault round, using only an
already-held local roster, or every other writer when none is available. A send
shares one ownership reading across its recipients, with a 3-second timeout and
a 5-second process cache (including failures). Cursor saves stat identity files
but decode them only after a stamp changes. Mirror refresh never acknowledges
a 5-second process cache (including failures). With effective sharing, cursor saves stat identity files
but decode them only after a stamp changes. Unpaired computers retain legacy
cursors, repairing reads and completion catalogs, with no sharing refresh clock
or mirror reads. The first team capture preserves the original file once as
`teams.before-sharing.json` before creating its ledger. Mirror refresh never acknowledges
local news and each hosted window carries its own mirror version.

## Binary size
Expand Down
4 changes: 2 additions & 2 deletions cmd/codeaf/app_devices_e14_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ func e14PairedEnginesRetry(t *testing.T, restart bool) {
t.Fatalf("A1: %d successful PUT attempts, want one per computer", puts.Load())
}
renamed := machineCall[appwire.DeviceRenameResult](t, wa, "devices.rename", appwire.DeviceRenameParams{Name: "studio"})
if renamed.Line != "This computer is now called studio." || puts.Load() != 3 {
if renamed.Line != "This computer is now called studio on all your devices." || puts.Load() != 3 {
t.Fatalf("A1/A3: rename was not published immediately: %+v, PUTs=%d", renamed, puts.Load())
}
appDeviceEvent(t, rb, func(v appwire.Devices) bool { return len(v.Devices) == 2 && v.Devices[1].Name == "studio" })
Expand All @@ -124,7 +124,7 @@ func e14PairedEnginesRetry(t *testing.T, restart bool) {
}
allow.Store(false)
pending := machineCall[appwire.DeviceRenameResult](t, wa, "devices.rename", appwire.DeviceRenameParams{Name: "office"})
if pending.Line != "This computer is now called office. Your other computers will see it once this computer is back online." {
if pending.Line != "This computer is now called office. Your other devices will show it after this one next connects." {
t.Fatalf("A3: pending rename receipt: %q", pending.Line)
}
allow.Store(true)
Expand Down
6 changes: 5 additions & 1 deletion cmd/codeaf/app_pair_merge_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,10 @@ func TestM1M2M5M9M10AppPairRealHomes(t *testing.T) {
return c, []byte(history)
}
chatA, wantA := seed(dirA, "A chat")
chatB, wantB := seed(dirB, "B chat")
// A startup-only joining home is legal; its own chat is created after pairing.
if _, err := identity.EnsureSolo(dirB); err != nil {
t.Fatal(err)
}
_, recA, wa := appDeviceBackend(t, dirA)
oldB, recB, wb := appDeviceBackend(t, dirB)
defer assertAppPairCopyM9(t, recA)
Expand All @@ -93,6 +96,7 @@ func TestM1M2M5M9M10AppPairRealHomes(t *testing.T) {
if err := oldB.Close(); err != nil {
t.Fatal(err)
}
chatB, wantB := seed(dirB, "B chat")
down.Store(true)
_, restarted, nextWire := appDeviceBackend(t, dirB)
wb = nextWire
Expand Down
4 changes: 2 additions & 2 deletions cmd/codeaf/app_relay_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ func TestM1M4M8M9M10AppPair(t *testing.T) {

func assertAppPairCopyM9(t *testing.T, rec *appRecorder) {
t.Helper()
allowed := regexp.MustCompile(`^(Pairing shares your chats with another computer\.|On your other computer, choose Pair a computer, then Enter a code from the other computer, and type [0-9-]+ \(dashes optional\)\. This code lasts 10 minutes\.|Waiting for approval on your other computer\. It should show: .+\.|Paired with .+\. Your chats can continue on both computers\.|Paired\. Your chats are kept\. Both computers' chats will appear as sharing finishes\.|Cancelled\. Nothing was paired\.|Pair with .+\?|Pair only if your other computer shows the same (three words|check number): .+\.|Pair|Don't pair|Open this pairing link on a computer you already use\.|Compare the check number on both computers: .+\.|Waiting for approval\. This link lasts [0-9]+ minutes\.)$`)
allowed := regexp.MustCompile(`^(Paired with your other device\. Run codeaf to open your chats\.|Pairing shares your chats with another computer\.|On your other computer, choose Pair a computer, then Enter a code from the other computer, and type [0-9-]+ \(dashes optional\)\. This code lasts 10 minutes\.|Waiting for approval on your other computer\. It should show: .+\.|Paired with .+\. Your chats can continue on both computers\.|Paired\. Your chats are kept\. Both computers' chats will appear as sharing finishes\.|Cancelled\. Nothing was paired\.|Pair with .+\?|Pair only if your other computer shows the same (three words|check number): .+\.|Pair|Don't pair|Open this pairing link on a computer you already use\.|Compare the check number on both computers: .+\.|Waiting for approval\. This link lasts [0-9]+ minutes\.)$`)
rec.mu.Lock()
defer rec.mu.Unlock()
for i, name := range rec.events {
Expand Down Expand Up @@ -295,7 +295,7 @@ func assertAppPairCopyM9(t *testing.T, rec *appRecorder) {

// The three check words are drawn at random from a word list that holds
// "ledger" and "cello", so only the copy around them is held to this list.
copyOnly := strings.ToLower(strings.ReplaceAll(line, p.Words, ""))
copyOnly := strings.ToLower(strings.ReplaceAll(strings.ReplaceAll(line, "Paired with your other device. Run codeaf to open your chats.", ""), p.Words, ""))
for _, banned := range []string{"codeaf pair", "--replace", "device", "identity", "cell", "lease", "fence", "ledger", "vault"} {
if strings.Contains(copyOnly, banned) {
t.Errorf("L3 terminal copy: %q", line)
Expand Down
34 changes: 28 additions & 6 deletions cmd/codeaf/app_take_recovery_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"errors"
"net/http"
"os"
"path/filepath"
Expand All @@ -14,14 +15,15 @@ import (
"github.com/Agent-Field/codeaf/internal/cell"
"github.com/Agent-Field/codeaf/internal/cell/celltest"
"github.com/Agent-Field/codeaf/internal/cellstore"
"github.com/Agent-Field/codeaf/internal/chatlist"
"github.com/Agent-Field/codeaf/internal/home"
"github.com/Agent-Field/codeaf/internal/identity"
"github.com/Agent-Field/codeaf/internal/session"
"github.com/Agent-Field/codeaf/internal/syncsetup"
)

// S5 uses L4's post-acquisition permission failure with a real open source
// conversation, then submits through the source app's ordinary send door.
// conversation. Dev requires fresh consent before that source may send again.
func TestAppTakeInstallFailureS5(t *testing.T) {
if runtime.GOOS == "windows" || os.Geteuid() == 0 {
t.Skip("L4 injection needs ordinary file permissions")
Expand Down Expand Up @@ -120,15 +122,35 @@ func TestAppTakeInstallFailureS5(t *testing.T) {
t.Fatalf("S5 failed taker row: %+v", rows)
}
t.Setenv(home.EnvVar, dirA)
if err = drive.started().CheckLease(t.Context()); err != nil {
t.Fatal(err)
}
var view appwire.Agent
source.locked(func() { view = source.agents[agent.ID].view })
appSyncWait(t, "released take is available without restoring work", func() bool {
if err := drive.started().CheckLease(t.Context()); err != nil {
t.Fatal(err)
}
source.locked(func() { view = source.agents[agent.ID].view })
return view.Location != nil && view.Location.Line == chatlist.Available
})
if view.Location != nil && strings.Contains(view.Location.Line, "continued") {
t.Fatalf("S5 released chat claims a continuation: %+v", view.Location)
}
turn, err := source.Send(agent.ID, "continue after the failed move", "s5-recover")
// A failed destination install releases the hold, not consent for this stale window.
if _, err := source.Send(agent.ID, "without fresh consent", "s5-refused"); err == nil {
t.Fatal("S5 stale source send was admitted without a new hold")
} else {
var failure *appwire.Error
if !errors.As(err, &failure) || failure.Code != appwire.CodeConflict {
t.Fatalf("S5 stale source refusal: %v", err)
}
}
current, err := source.ListChats()
if err != nil || len(current.Chats) != 1 || !current.Chats[0].CanTake {
t.Fatalf("S5 source continuation: %+v, %v", current, err)
}
resumed, err := source.TakeAgent(appwire.TakeParams{Cell: c.ID, Seen: current.Chats[0].Seen})
if err != nil {
t.Fatalf("S5 explicit source continuation: %v", err)
}
turn, err := source.Send(resumed.ID, "continue after the failed move", "s5-recover")
if err != nil {
t.Fatalf("S5 source send: %v", err)
}
Expand Down
5 changes: 5 additions & 0 deletions cmd/codeaf/apperrand.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ func (b *appBackend) Errand(ctx context.Context, id string, ask session.ErrandAs
return session.ErrandAnswer{}, err
}
}
// Factory page calls and the brief writer borrow the agent for one errand.
// Ordinary calls on that same agent retain their refusal policy.
if session.IsFactoryAIPurpose(ask.Purpose) {
ask.DropRefusedEffort = true
}
answer, err := door.RunErrand(ctx, ask)
if err == nil || errors.Is(err, session.ErrErrandAsk) || errors.Is(err, session.ErrErrandRoute) {
return answer, err
Expand Down
9 changes: 6 additions & 3 deletions cmd/codeaf/appfactory_memory_spend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -483,11 +483,14 @@ func TestMemorySpendAndLifecycle(t *testing.T) {
failUpdates.Store(true)
defer failUpdates.Store(false)
before := calls.Load()
ctx, cancel := context.WithTimeout(context.Background(), 6500*time.Millisecond)
// Three-attempt rule: an unchanged failed input makes exactly three attempts,
// ordinaryBuildAttemptLimit in internal/knowledge/build.go. Failures wait 5, 10
// and 20 seconds, so a fourth attempt would come at about 35 seconds.
ctx, cancel := context.WithTimeout(context.Background(), 38*time.Second)
defer cancel()
_ = v.Run(ctx, history, func() string { return "fixture" })
if got := calls.Load() - before; got != 1 {
t.Errorf("unchanged failed input spent %d model calls, want 1", got)
if got := calls.Load() - before; got != 3 {
t.Errorf("unchanged failed input spent %d model calls, want exactly 3 and no further retries", got)
}
})
t.Run("unchanged refinement", func(t *testing.T) {
Expand Down
27 changes: 24 additions & 3 deletions cmd/codeaf/chatv3.go
Original file line number Diff line number Diff line change
Expand Up @@ -1028,6 +1028,7 @@ func v3ConfigFor(proc *v3Process, opts v3Options, workspace string, place sessio

cfg := session.Config{
TeamComputers: teamComputers,
TeamsPaired: teamPaired,
Workspace: workspace,
Model: chosen,
APIKey: settings.APIKey,
Expand Down Expand Up @@ -2006,7 +2007,7 @@ func v3Policy(workspace, profileDir string, yolo bool) (*approval.Policy, error)
// ordinary launch — so the exceptions, the shell rules and the floor cannot
// differ between them.
func v3PolicyMode(workspace, profileDir, mode string) (*approval.Policy, error) {
rows, err := v3ApprovalRowsAt(workspace, profileDir, mode)
rows, err := v3ApprovalRowsAt(workspace, profileDir, mode, false)
if err != nil {
return nil, err
}
Expand All @@ -2018,15 +2019,32 @@ func v3PolicyMode(workspace, profileDir, mode string) (*approval.Policy, error)
// configuration reader's file freshness rules.
type v3ApprovalRows struct {
mode, tools, bash string
profileReadError string
}

func v3ApprovalRowsAt(workspace, profileDir, mode string) (v3ApprovalRows, error) {
func v3ApprovalRowsAt(workspace, profileDir, mode string, headless bool) (rows v3ApprovalRows, err error) {
project, err := config.LoadProjectConfig(workspace)
if err != nil {
return v3ApprovalRows{}, err
}
// Every profile read participates, including a read that fails after an
// earlier row succeeded. Returning an ordinary error would retain an old,
// potentially permissive live policy instead of asking the person.
defer func() {
var pathErr *os.PathError
if errors.As(err, &pathErr) && pathErr.Path == config.BudgetConfigPath(profileDir) {
rows, err = v3ApprovalRows{profileReadError: err.Error()}, nil
}
}()
if _, err := config.ReadProfileApprovalConfig(profileDir); err != nil {
return v3ApprovalRows{}, err
}
if mode == "" {
mode, err = project.ResolveString(profileDir, config.KeyToolApprovalMode)
if headless {
mode, err = config.HeadlessToolApprovalModeAt(workspace, profileDir)
} else {
mode, err = project.ResolveString(profileDir, config.KeyToolApprovalMode)
}
if err != nil {
return v3ApprovalRows{}, err
}
Expand All @@ -2043,6 +2061,9 @@ func v3ApprovalRowsAt(workspace, profileDir, mode string) (v3ApprovalRows, error
}

func (rows v3ApprovalRows) build() (*approval.Policy, error) {
if rows.profileReadError != "" {
return &approval.Policy{ProfileReadError: rows.profileReadError}, nil
}
raw := map[string]any{"default": rows.mode}
// The repository's exceptions REPLACE the person's, whole (internal/config
// states the merge law): a rule set assembled from two files is a rule set
Expand Down
13 changes: 5 additions & 8 deletions cmd/codeaf/chatv3_approval.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,17 +80,14 @@ func (g *v3ApprovalGate) Build(posture string) (*approval.Policy, bool, error) {
case session.PostureDeny:
mode, guardian = string(approval.ActionDeny), false
}
if mode == "" && g.headless {
var err error
mode, err = config.HeadlessToolApprovalModeAt(g.workspace, g.profileDir)
if err != nil {
return nil, false, err
}
}
rows, err := v3ApprovalRowsAt(g.workspace, g.profileDir, mode)
rows, err := v3ApprovalRowsAt(g.workspace, g.profileDir, mode, g.headless)
if err != nil {
return nil, false, err
}
if rows.profileReadError != "" {
guardian = false
}

g.mu.Lock()
defer g.mu.Unlock()
if g.policy != nil && g.rows == rows {
Expand Down
18 changes: 15 additions & 3 deletions cmd/codeaf/chatv3_approval_live_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,23 @@ func TestApprovalGateRejectsUnreadableProfiles(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if policy, _, err := gate.Build(posture); err == nil || policy != nil {
t.Fatalf("%s profile produced policy %+v, error %v; want a failed build", broken, policy, err)
policy, _, err := gate.Build(posture)
if broken == "malformed" {
if err == nil || policy != nil {
t.Fatalf("%s profile produced policy %+v, error %v; want a failed build", broken, policy, err)
}
} else {
if err != nil || policy == nil {
t.Fatalf("unreadable profile must keep interactive startup available: %v", err)
}
for _, tool := range []string{"write", "read", "manual", "use_service", "unknown"} {
if got := policy.Check(tool, nil); got.Action != approval.ActionPrompt {
t.Errorf("%s = %s; want prompt", tool, got.Action)
}
}
}
if first.Check("write", json.RawMessage(`{"path":"example.go"}`)).Action != approval.ActionDeny {
t.Fatal("failed rebuild changed the previous policy")
t.Fatal("rebuild changed the immutable previous policy")
}
})
}
Expand Down
17 changes: 16 additions & 1 deletion cmd/codeaf/chatv3_host_teams.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ func hostTeamsSeam(far hostFar, welcome remote.Welcome) tui3.TeamsSeam {
// when the engine answers them, so an older engine's window has a seam whose
// delegation doors are nil, which the surface says rather than guesses at.
func (h *hostTeams) seam() tui3.TeamsSeam {
s := tui3.TeamsSeam{Load: h.load, ReadSince: h.readSince, Update: h.update, Traffic: h.readTraffic}
s := tui3.TeamsSeam{Sharing: h.sharing, Load: h.load, ReadSince: h.readSince, Update: h.update, Traffic: h.readTraffic}
if h.history != nil {
s.DisplayTraffic = func(team, after, version string, limit int) ([]teamstore.Entry, string, string, bool, error) {
got, err := h.history(team, after, version, limit)
Expand Down Expand Up @@ -306,3 +306,18 @@ func (h *hostTeams) forgetChecked(team string, expected []string) ([]string, err
}
return reply.Gone, nil
}

// sharing asks the engine rather than borrowing the viewing computer's pairing.
func (h *hostTeams) sharing() (bool, error) {
_, stamp, _ := h.held(nil)
reading, err := h.read(stamp, h.reservedHues())
if err != nil {
return false, err
}
if reading.Sharing != nil {
return *reading.Sharing, nil
}
// Older engines gate their own history. Keep that display door available
// when they cannot report the gate separately.
return h.history != nil, nil
}
Loading