Skip to content

Commit 22be879

Browse files
committed
Guard outgoing transfer actor and default to tracked principal
1 parent 5fde074 commit 22be879

File tree

4 files changed

+104
-6
lines changed

4 files changed

+104
-6
lines changed

docs/AGENT-PIPE-TEST-LOG.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Agent Pipe Test Log (Warm Idris)
2+
3+
Purpose: capture real-world StackFlow pipe test outcomes so onboarding for other agents is based on observed behavior, not assumptions.
4+
5+
## Per-test template
6+
7+
- Timestamp (UTC):
8+
- Counterparty:
9+
- Pipe identifier / contract:
10+
- Scenario:
11+
- Preconditions:
12+
- Action executed:
13+
- Expected result:
14+
- Observed result:
15+
- Artifacts (txid, signatures, nonce, logs):
16+
- Pass/Fail:
17+
- Root cause (if fail):
18+
- Fix / mitigation:
19+
- Process improvement for future agents:
20+
21+
---
22+
23+
## Run log
24+

packages/stackflow-agent/README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ await agent.openPipe({
106106
const outgoing = agent.buildOutgoingTransfer({
107107
pipeId: tracked.pipeId,
108108
amount: "25",
109-
actor: tracked.localPrincipal,
109+
// actor defaults to tracked.localPrincipal
110110
});
111111
```
112112

@@ -135,5 +135,6 @@ const result = await agent.acceptIncomingTransfer({
135135
4. Watcher retries are idempotent for already-disputed closures (same closure txid is skipped on later polls).
136136
5. Read-only polling isolates per-pipe failures (`getPipeState` errors on one pipe do not stop others).
137137
6. Event scan mode intentionally holds the cursor when any dispute submission errors occur, so failed disputes are retried on next run.
138-
7. Incoming transfer validation enforces tracked contract/pipe/principals/token consistency; mismatched `pipeId`, `pipeKey`, `actor`, or token payloads are rejected.
139-
8. For production hardening, add alerting, signer balance checks, and idempotency audit logs.
138+
7. `buildOutgoingTransfer(...)` defaults `actor` to the tracked local principal and rejects mismatched actor values.
139+
8. Incoming transfer validation enforces tracked contract/pipe/principals/token consistency; mismatched `pipeId`, `pipeKey`, `actor`, or token payloads are rejected.
140+
9. For production hardening, add alerting, signer balance checks, and idempotency audit logs.

packages/stackflow-agent/src/agent-service.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ export class StackflowAgentService {
8888
buildOutgoingTransfer({
8989
pipeId,
9090
amount,
91-
actor,
91+
actor = null,
9292
action = "1",
9393
secret = null,
9494
validAfter = null,
@@ -134,6 +134,14 @@ export class StackflowAgentService {
134134
const nextTheir = currentTheir + transferAmount;
135135
const nextNonce = currentNonce + 1n;
136136

137+
const normalizedActor =
138+
actor == null || String(actor).trim() === ""
139+
? tracked.localPrincipal
140+
: assertNonEmptyString(actor, "actor");
141+
if (normalizedActor !== tracked.localPrincipal) {
142+
throw new Error("actor must match tracked local principal");
143+
}
144+
137145
return {
138146
contractId: tracked.contractId,
139147
pipeKey: tracked.pipeKey,
@@ -144,7 +152,7 @@ export class StackflowAgentService {
144152
theirBalance: nextTheir.toString(10),
145153
nonce: nextNonce.toString(10),
146154
action: toUnsignedString(action, "action"),
147-
actor: assertNonEmptyString(actor, "actor"),
155+
actor: normalizedActor,
148156
secret,
149157
validAfter,
150158
beneficialOnly: beneficialOnly === true,

tests/stackflow-agent.test.ts

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -965,9 +965,9 @@ describe("stackflow agent", () => {
965965
const outgoing = agent.buildOutgoingTransfer({
966966
pipeId,
967967
amount: "25",
968-
actor: "ST1LOCAL",
969968
});
970969

970+
expect(outgoing.actor).toBe("ST1LOCAL");
971971
expect(outgoing.myBalance).toBe("75");
972972
expect(outgoing.theirBalance).toBe("25");
973973
expect(outgoing.nonce).toBe("1");
@@ -992,6 +992,71 @@ describe("stackflow agent", () => {
992992
store.close();
993993
});
994994

995+
it("rejects outgoing transfer requests with actor mismatch", () => {
996+
const dbFile = tempDbFile("agent-send-actor-mismatch");
997+
const store = new AgentStateStore({ dbFile });
998+
999+
const contractId = "ST1TESTABC.contract";
1000+
const pipeKey = {
1001+
"principal-1": "ST1LOCAL",
1002+
"principal-2": "ST1OTHER",
1003+
token: null,
1004+
};
1005+
const pipeId = buildPipeId({ contractId, pipeKey });
1006+
1007+
store.upsertTrackedPipe({
1008+
pipeId,
1009+
contractId,
1010+
pipeKey,
1011+
localPrincipal: "ST1LOCAL",
1012+
counterpartyPrincipal: "ST1OTHER",
1013+
token: null,
1014+
});
1015+
1016+
store.upsertSignatureState({
1017+
contractId,
1018+
pipeKey,
1019+
forPrincipal: "ST1LOCAL",
1020+
withPrincipal: "ST1OTHER",
1021+
token: null,
1022+
myBalance: "100",
1023+
theirBalance: "0",
1024+
nonce: "0",
1025+
action: "1",
1026+
actor: "ST1LOCAL",
1027+
mySignature: "0x" + "11".repeat(65),
1028+
theirSignature: "0x" + "22".repeat(65),
1029+
secret: null,
1030+
validAfter: null,
1031+
beneficialOnly: false,
1032+
});
1033+
1034+
const agent = new StackflowAgentService({
1035+
stateStore: store,
1036+
signer: {
1037+
async sip018Sign() {
1038+
return "0x" + "44".repeat(65);
1039+
},
1040+
async submitDispute() {
1041+
return { txid: "0x1" };
1042+
},
1043+
async callContract() {
1044+
return { ok: true };
1045+
},
1046+
},
1047+
});
1048+
1049+
expect(() =>
1050+
agent.buildOutgoingTransfer({
1051+
pipeId,
1052+
amount: "25",
1053+
actor: "ST1THIRD",
1054+
}),
1055+
).toThrow("actor must match tracked local principal");
1056+
1057+
store.close();
1058+
});
1059+
9951060
it("defaults watcher interval to one hour", () => {
9961061
const dbFile = tempDbFile("agent-interval");
9971062
const store = new AgentStateStore({ dbFile });

0 commit comments

Comments
 (0)