Notify Tentacles when pending Remote approval is revoked

This commit is contained in:
ak47 committed 2026-11-04 14:43:22 -05:00
1 parent 205cb27483
commit 9bc74dbb13
8 files changed
+61 -12

No files matched your search

+4 -2
View File
@@ -154,9 +154,11 @@ Its sequence starts at one and increases exactly once per event. Supported kinds
- `remote.action`: sequence, kind, action token, host-issued epoch, role and approved
grant subset. Created only by the internal core authorizer. Approval expires
after 30 seconds if not consumed. No network secret is included.
- `remote.revoked`: sequence, kind, lease, epoch and reason (`closed`, `expired`,
- `remote.revoked`: sequence, kind, epoch, exactly one of action or lease, and reason (`closed`, `expired`,
`local`, `replaced`). Failure to enqueue revocation retires the session; it
cannot block immediate cancellation. An unconsumed approval expires silently.
cannot block immediate cancellation. Pending approval expiry/revocation carries
its action token; an opened lease carries its lease token. Both retire external
setup workers through the same epoch-bound notification.
Child calls:
+2 -2
View File
@@ -43,8 +43,8 @@ func (runtime *tentacleRuntime) interact(ctx context.Context, command tentacleCo
defer stopCommand()
stop := context.AfterFunc(command.session.OutputContext(), cancel)
defer stop()
// Policy cancellation can interrupt setup before a lease exists, when no
// remote.revoked event is emitted. Tell the original continuation to stop its
// Policy cancellation can interrupt setup before approval exists.
// Tell the original continuation to stop its
// inspection/setup workers before releasing the interaction slot. Never pass
// cancelled form values or reuse the cancelled interaction context.
cleanupNext := ""
+2 -2
View File
@@ -182,8 +182,8 @@ func (session *Session) revoke_remote_locked(reason string, notify bool) {
access.timer.Stop()
}
session.release_remote_owner()
if notify && access.lease != "" {
event := tentaclewire.SessionEvent{Kind: "remote.revoked", Lease: access.lease, Epoch: access.epoch, Reason: reason}
if notify {
event := tentaclewire.SessionEvent{Kind: "remote.revoked", Action: access.action, Lease: access.lease, Epoch: access.epoch, Reason: reason}
if !session.remote_event_locked(&event) {
session.fail_locked(ErrLimit)
}
+4 -4
View File
@@ -75,10 +75,10 @@ func TestRemotePolicyRevokesPendingAndLiveSDKAccess(t *testing.T) {
if _, code := remoteCall(t, session, "remote.heartbeat", tentaclewire.RemoteOperation{Lease: lease.Lease, Epoch: lease.Epoch, Sequence: 1}); code != -32001 {
t.Fatal("heartbeat revived lease", code)
}
var event tentaclewire.SessionEvent
if json.Unmarshal(invokeSessionSDK(t, session, "event", nil), &event) != nil || event.Kind != "remote.revoked" || event.Lease != lease.Lease {
t.Fatal("revocation event", event)
}
}
var event tentaclewire.SessionEvent
if json.Unmarshal(invokeSessionSDK(t, session, "event", nil), &event) != nil || event.Kind != "remote.revoked" || event.Epoch != action.Epoch || event.Lease != lease.Lease || (!opened && event.Action != action.Action) {
t.Fatal("revocation event", event)
}
}
// A fresh approval can replace the cancelled epoch even if its
+30
View File
@@ -470,3 +470,33 @@ func TestSessionAggregateQueueBound(t *testing.T) {
t.Fatal("aggregate byte ceiling exceeded")
}
}
func TestPendingRemoteRevocationReachesBothSDKs(t *testing.T) {
for _, language := range []string{"go", "python"} {
for _, reason := range []string{"local", "expired", "replaced"} {
t.Run(language+"/"+reason, func(t *testing.T) {
session := startSessionSDK(t, test_manager(t, DefaultLimits(), nil), language, "pending-revoke")
action, err := session.AuthorizeRemote("controller", []string{"remote-present"})
if err != nil {
t.Fatal(err)
}
invokeSessionSDK(t, session, "event", nil)
session.mutex.Lock()
if reason == "expired" {
session.refresh_remote_locked(time.Millisecond)
} else {
session.revoke_remote_locked(reason, true)
}
session.mutex.Unlock()
var event tentaclewire.SessionEvent
raw := invokeSessionSDK(t, session, "event", nil)
if json.Unmarshal(raw, &event) != nil || event.Kind != "remote.revoked" || event.Action != action.Action || event.Lease != "" || event.Epoch != action.Epoch || event.Reason != reason {
t.Fatal("pending action revocation was not delivered", string(raw))
}
if _, code := remoteCall(t, session, "remote.open", tentaclewire.RemoteOpen{Action: action.Action, Epoch: action.Epoch, Role: action.Role, Grants: action.Grants}); code != -32001 {
t.Fatal("retired action opened a lease", code)
}
})
}
}
}
+1 -1
View File
@@ -99,7 +99,7 @@ func ValidSessionEvent(event SessionEvent) bool {
case "remote.action":
return event.Speech == nil && len(event.Action) == 32 && event.Lease == "" && event.Reason == "" && ValidRoleGrants(event.Role, event.Grants)
case "remote.revoked":
return event.Speech == nil && event.Action == "" && len(event.Lease) == 32 && event.Role == "" && len(event.Grants) == 0 && slices.Contains([]string{"closed", "expired", "local", "replaced"}, event.Reason)
return event.Speech == nil && ((event.Action == "" && len(event.Lease) == 32) || (len(event.Action) == 32 && event.Lease == "")) && event.Role == "" && len(event.Grants) == 0 && slices.Contains([]string{"closed", "expired", "local", "replaced"}, event.Reason)
case "remote.output":
return event.Action == "" && len(event.Lease) == 32 && event.Role == "" && len(event.Grants) == 0 && event.Reason == "" && event.Speech != nil && ValidRemoteSpeech(*event.Speech)
}
+15
View File
@@ -33,3 +33,18 @@ func TestSessionNegotiationRequiresIndependentGrants(t *testing.T) {
})
}
}
func TestRemoteRevocationIdentifiesExactlyOneActionOrLease(t *testing.T) {
token := "0123456789abcdef0123456789abcdef"
for _, item := range []struct {
action, lease string
valid bool
}{
{token, "", true}, {"", token, true}, {token, token, false}, {"", "", false}, {"short", "", false},
} {
event := SessionEvent{Sequence: 1, Epoch: 1, Kind: "remote.revoked", Reason: "expired", Action: item.action, Lease: item.lease}
if ValidSessionEvent(event) != item.valid {
t.Fatalf("unexpected validation for %+v", item)
}
}
}
+3 -1
View File
@@ -305,7 +305,9 @@ class SessionClient:
or role == "controlled" and set(grants) & {"remote-present", "keyboard-capture"}):
raise ProtocolError("Invalid action event")
elif event["kind"] == "remote.revoked":
if ("speech" in event or not _token(event.get("lease")) or event.get("action", "") != "" or event.get("role", "") != ""
identity = ((_token(event.get("lease")) and event.get("action", "") == "")
or (_token(event.get("action")) and event.get("lease", "") == ""))
if ("speech" in event or not identity or event.get("role", "") != ""
or event.get("grants", []) != [] or event.get("reason") not in ("closed", "expired", "local", "replaced")):
raise ProtocolError("Invalid revoke event")
elif event["kind"] == "remote.output":